/*
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package server;

import java.io.*;
import java.net.*;
import java.util.Timer;
import java.util.TimerTask;

// Trida obsluhujici kazdeho klienta, ktery se pripoji na server
public class ClientThread extends Thread
{
	private Socket socket; // promenna pro socket
	private BufferedOutputStream bout = null; // promenna slouzici k odesilani dat
	private BufferedInputStream bin = null; // promenna slouzici ke cteni dat
	private String name = "User" + getId(); // Jmeno klienta, defaultne "User"+ID vlakna
	private Timer timer = new Timer(); // Promenna pro odpocet casu pro synchronizaci seznamu
										// uzivatelu
	private Timer maintenance = new Timer(); // Promenna pro odpocet casu pro kontrolu odpojenych
												// klientu
	private long lastAction; // posledni akce uzivate (posledni cas prijmuti 0x09 packetu)

	public ClientThread(Socket socket)
	{
		this.socket = socket; // ulozeni socketu, na kterem je klient pripojen
		try
		{
			bout = new BufferedOutputStream(socket.getOutputStream()); // navazani spojeni s
																		// odesilacim kanalem
			bin = new BufferedInputStream(socket.getInputStream()); // navazani spojeni s prijmacim
																	// kanalem
		}
		catch (IOException e) // zachyceni vyjimky
		{
			e.printStackTrace(System.err); // vypis vyjimky do konzole
			close(false); // Uzavreni klientskeho vlakna po chybe
		}
	}

	// Metoda, ktera odesila zpravy vsem klientum
	public synchronized void broadcast(String message, ClientThread from)
	{
		String nick = from.getClientName(); // Zjisteni odesilatelova jmena
		for (ClientThread clt : Server.getClients()) // Prohazeni vsech klientu foreach cyklem
		{
			try
			{
				// odeslani zpravy ve formatu: KodAkce + OdesilateluvNick + Zprava
				clt.writeData(Sifrovani.encrypt("0x00" + nick + ": " + message));
			}
			catch (Exception e)
			{
			}
		}
	}

	// Pokud je zprava soukroma odesle se touto metodou
	public synchronized void sendPM(String message, String to)
	{
		for (ClientThread clt : Server.getClients()) // Prochazeni vsech uzivatelu
		{
			if (clt.getClientName().equals(to)) // Pokud uzivateluv nick odpovida adresatovi
												// soukrome zpravy
			{
				try
				{
					// odesle se zprava
					clt.writeData(Sifrovani.encrypt("0x00" + "(whisper from " + getClientName() + "): " + message));
				}
				catch (IOException e)
				{
					e.printStackTrace();
				}
			}// pokud ne, nestane se nic
		}
		try
		{
			// odeslani zpravy take odesilateli
			writeData(Sifrovani.encrypt("0x00" + "(whisper to " + to + "): " + message));
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
	}

	// Odeslani oznameni o odpojeni/pripojeni uzivatele
	public synchronized void broadcastChangedClientStatus(ClientThread from, boolean justConnected)
	{
		String nick = from.getClientName(); // Zjisteni nicku odesilatele
		for (ClientThread clt : Server.getClients()) // Prochazeni vsech pripojenych uzivatelu
		{
			if (clt == from) // pokud prave tento klient je ten, ktery se odpojil/pripojil
				continue; // preskoc k dalsimu klientovi
			try
			{
				if (justConnected) // Pokud se klient pripojil
					clt.writeData(Sifrovani.encrypt("0x00" + nick + " connected.")); // odesle se
																						// zprava o
																						// pripojeni
				else
					// pokud ne
					clt.writeData(Sifrovani.encrypt("0x00" + nick + " disconnected.")); // odesle se
																						// zprava o
																						// odpojeni
			}
			catch (Exception e)
			{
			}
		}
	}

	// Metoda slouzici ke cteni zprav od klienta
	@Override
	public void run()
	{
		sendPhrase(); // Synchronizace sifrovaciho klice pomoci Vernamovy sifry
		maintenancePacketInit(); // Inicializace udrzovaciho packetu
		userlistSyncInit(); // Inicializace synchronizace seznamu uzivatelu odesilanych klientovi
		try
		{
			while (!isInterrupted()) // opakovat dokud neni vlakno preruseno
			{

				String data = readData(); // cekani na prijeti dat

				if (data == null) // pokud jsou data prazdna
					continue; // preskoc na konec cyklu

				int opcode = Integer.parseInt(data.substring(2, 4)); // Rozdeleni Kodu akce
				String message = data.substring(4); // a samotne zpravy

				switch (opcode)
				// podle typu se provede prislusna akce
				{
					case 0: // obycejna zprava
						broadcast(Sifrovani.decrypt(message), this); // odeslat zpravu vsem
																		// pripojenym klientum
						break; // ukonceni switche

					case 1: // tato zprava znaci, ze se klient odpojuje
						throw new IOException("Shutting down"); // vyhodit vyjimku a tim ukoncit
																// toto klientske vlakno

					case 2: // nastaveni uzivatelovo jmena
						if (!message.equals("NULL")) // pokud uzivatel nejake jmeno zadal
							setClientName(message); // nastav jej. Pokud ne je pouzito cislo vlakna
													// jako jmeno
						broadcastChangedClientStatus(this, true); // oznameni o pripojeni noveho
																	// klienta
						break;
					case 8: // soukroma zprava
						try
						{
							String to = message.split(" ")[1]; // zjisteni prijemce
							String pm = message.split(" ")[2]; // a zpravy z prichozich dat
							sendPM(pm, to); // odeslani zpravy
						}
						catch (Exception e)
						{
						}
						break;
					case 9: // udrzovaci packet
						lastAction = System.currentTimeMillis(); // ulozeni casu posledni akce
						break; // tento packet je posilan po 1s
					default: // pokud cislo neodpovida ani jedne akci
						break; // nedelej nic
				}

				try
				{
					Thread.sleep(100); // uspani vlakna na 100ms
				}
				catch (InterruptedException e)
				{
				}
			}
		}
		catch (Exception e)
		{

		}
		finally
		// at uz metoda vyhodi vyjimku nebo ne
		{
			broadcastChangedClientStatus(this, false); // vzdy se odesle zprava o odpojeni
			close(false); // klient je odpojen a vlakno zruseno
		}
	}

	// metoda pro synchronizaci seznamu pripojenych uzivatelu
	private void userlistSyncInit()
	{
		timer.scheduleAtFixedRate(new TimerTask() // vytvoreni opakujici se udalosti
		{
			@Override
			public void run()
			{
				try
				{
					String clients = ""; // promenna pro jmena vsech klientu
					for (ClientThread clt : Server.getClients())
						// prochazeni klientu
						clients = clients + clt.getClientName() + ","; // pridani jmen vsech klientu
																		// do jednoho stringu
					writeData(Sifrovani.encrypt("0x07" + clients)); // odeslani seznamu klientovi
				}
				catch (IOException e)
				{
				}
			}
		}, 1000, 1000); // prodleva pred prvnim spustenim, opakovat po. Oba udaje jsou v milisec.
	}

	// Metoda pro kontrolu jestli uzivatel nezustal "viset" na serveru
	private void maintenancePacketInit()
	{
		maintenance.scheduleAtFixedRate(new TimerTask() // vytvoreni opakujici se udalosti
		{
			@Override
			public void run()
			{
				if (lastAction - System.currentTimeMillis() > 5000) // pokud se uzivatel vice jak 5s
																	// neohlasil
				{
					close(false); // je odpojen
				}
			}
		}, 5000, 1000); // prodleva pred prvnim spustenim, opakovat po. Oba udaje jsou v milisec.
	}

	// Metoda pro odpojeni klienta
	public void close(boolean isBlocked)
	{
		try
		{
			writeData(Sifrovani.encrypt("0x01")); // Odeslani oznameni o odpojeni klienta
			timer.cancel(); // Zruseni opakovani synchronizace
			maintenance.cancel(); // zruseni kontroly odpojeni
			bout.close(); // uzavreni odesilaciho kanalu
			bin.close(); // uzavreni prijmaciho kanalu
			socket.close(); // uzavreni socketu
			if (!isBlocked) // zalezi na promenne zda bude klient odebran ze seznamu
				Server.RemoveClient(this); // odebrani klienta ze seznamu, pokud je promenna
											// isBlocked true
											// znamena to, ze metoda close je vyvolana z cyklu,
											// ktery
											// prochazi vsechny klienty. K seznamu klientu se muze
											// pristupovat
											// pouze 1x ve stejny okamzik a proto nemuze byt metoda
											// RemoveClient
											// zavolana pokud je isBlocked true.
		}
		catch (Exception e)
		{
		}

		interrupt(); // ukonceni vlakna
	}

	// Synchronizace sifrovaciho klice pomoci Vernamovy sifry
	private void sendPhrase()
	{
		try
		{
			Thread.sleep(100); // Pozastaveni behu vlakna na 100ms
			// 1st pass
			String phrase = Sifrovani.getPhrase(); // zjisteni vygenerovaneho sifrovaciho klice
			Vernam v = new Vernam(); // vytvoreni nove instance vernamovy sifry
			v.setPhrase(phrase); // nastaveni sifrovaciho klice do teto instance
			v.generateKey(); // vygenerovani nahodneho klice, kterym bude sirovaci klic zasifrovan
			writeData(v.XOR()); // provedeni XOR mezi sifrovacim klicem a docasnym klicem a odeslani
								// klientovi
			Thread.sleep(100); // uspani vlakna na 100ms
			// 2nd pass
			String b = readData(); // cteni zpravy od klienta (ta je zasifrovana vygenerovanym
									// klicem klienta)
			Thread.sleep(100); // upsani vlakna na 100ms
			v.setPhrase(b); // nastaveni prijmute zpravy do instance sifry
			writeData(v.XOR()); // odsifrovani a odeslani (tato zprava je ted zasifrovana pouze
								// klicem klienta)
			Thread.sleep(100); // uspani vlakna na 100ms
			v.forget(); // vymazani zpravy a docasneho klice z pameti
		}
		catch (IOException e)
		{
		}
		catch (Exception e)
		{
			e.printStackTrace(); // vypsani chyby
		}
	}

	// Posila data pres sit ke klientovi
	private void writeData(String s) throws IOException
	{
		byte[] msg = s.getBytes(); // rozdeleni zpravy na pole typu byte
		int len = msg.length + 2; // zjisteni velikosti zpravy
		bout.write(len & 0xff); // odeslani velikosti zpravy
		bout.write(len >> 8 & 0xff); // aby klient vedel jak dlouho ma cist a zda mu prisla zprava cela
		bout.write(msg); // odeslani samotne zpravy
		bout.flush(); // uvolneni odesilaci pameti
	}

	// Prijma data pres sit od klienta
	private String readData() throws IOException
	{
		int lengthLo = bin.read(); // cteni spodnich bitu delky
		int lengthHi = bin.read(); // cteni hornich bitu delky
		int length = lengthHi * 256 + lengthLo; // vypocet celkove delky

		byte[] data = new byte[length - 2]; // vytvoreni docasneho pole, kam se bude zprava ukladat

		int receivedBytes = 0; // pocet prenesenych dat
		int newBytes = 0; // cislo prenasenych dat (-1 pokud dosahne konce streamu)
		int left = length - 2; // zbyvajici pocet dat
		while (newBytes != -1 && receivedBytes < length - 2) // dokud cteni nedostahne konce streamu
		{
			// cteni dat. metoda read je rekurzivni, proto se uklada do vstupniho pole, vraci
			// poradove cislo
			newBytes = bin.read(data, receivedBytes, left); // prectenych dat
			receivedBytes = receivedBytes + newBytes; // ulozeni noveho poctu prenesenych dat
			left -= newBytes; // ulozeni noveho poctu zbyvajicich dat
		}
		return new String(data); // prevod zpravy do Stringu a navraceni
	}

	// metoda pro nastaveni klientova jmena
	private void setClientName(String name)
	{
		this.name = name;
	}

	// metoda pro zjisteni klientova jmena
	public String getClientName()
	{
		return name;
	}
}